Skip to content

fix: isolate empty-mention waiter by sender without changing default session scope - #9378

Open
hedssaz wants to merge 3 commits into
AstrBotDevs:masterfrom
hedssaz:fix/9377
Open

fix: isolate empty-mention waiter by sender without changing default session scope#9378
hedssaz wants to merge 3 commits into
AstrBotDevs:masterfrom
hedssaz:fix/9377

Conversation

@hedssaz

@hedssaz hedssaz commented Jul 24, 2026

Copy link
Copy Markdown

Fixes #9377

In group chats, an empty mention (@bot with no text) starts a 60s
session_waiter to wait for follow-up content. The waiter is registered with
DefaultSessionFilter, whose key is unified_msg_origin (group-level) and does
not include the sender. As a result, the first non-empty message from any
other member
during the wait window is intercepted, gets a bot At prepended,
and is re-dispatched — producing a reply to the wrong user / wrong context. Full
analysis, reproduction and captured logs are in #9377.

This PR fixes the empty-mention flow without changing the default session
scope
, so third-party plugins that rely on the current group-level default are
unaffected.

Why not simply change DefaultSessionFilter

Changing the default from unified_msg_origin to include the sender would
change the default runtime behavior of session_waiter, which is exported to
plugins through astrbot.api.util. The group-level default has been in place
since v3.5.4 (~15 months, 138 stable releases) and is relied upon by real
plugins that omit session_filter=, for example:

  • astrbot_plugin_kimi_datasource_api registers a waiter manually with
    SessionWaiter(DefaultSessionFilter(), event.unified_msg_origin, ...). The
    registration key is the raw UMO while lookups go through
    DefaultSessionFilter.filter(event). If the default changed, the keys would no
    longer match and its login waiter would never trigger.
  • astrbot_plugin_decision_roulette and astrbot_plugin_TurtleSoup use the
    default filter to collect input from multiple group members; a per-sender
    default would only accept the initiator.

So the fix is scoped to the one place that actually needs per-sender isolation.

Modifications

  • astrbot/core/utils/session_waiter.py: add SenderSessionFilter, a new
    optional SessionFilter keyed by unified_msg_origin + sender_id.
    DefaultSessionFilter is unchanged, and no new names are added to the
    public astrbot.api.util surface. This is purely additive.
    The key uses a length-prefixed encoding (f"{len(umo)}:{umo}:{sender}")
    rather than plain concatenation: both the UMO and the sender id are arbitrary
    platform strings (e.g. Telegram topic-group UMOs contain #), so a plain
    separator would allow constructible key collisions that defeat the isolation.
  • astrbot/builtin_stars/astrbot/main.py: the empty-mention waiter now passes
    session_filter=SenderSessionFilter(). When the sender id is unavailable it
    skips waiting entirely, to avoid silently degrading back to group scope.
  • tests/test_session_waiter_cross_user.py (new): 7 session-key regression
    tests (module loaded standalone).
  • tests/unit/test_empty_mention_sender_scope.py (new): 3 production-wiring
    tests driving the real Main.handle_empty_mention and
    Main.handle_session_control_agent (following the existing
    Main.__new__(Main) pattern in tests/unit/test_group_chat_context_wiring.py).

The other issues noted in #9377 (empty-message_str messages being swallowed
during the wait window; _cleanup / FILTERS behavior with same-key waiters)
are independent of the session key and will be handled in separate PRs.

Screenshots or Test Results

$ ruff format --check <changed files + test files> && ruff check <same>
All checks passed!

$ pytest tests/test_session_waiter_cross_user.py \
         tests/unit/test_empty_mention_sender_scope.py -o asyncio_mode=auto -q
..........                                                               [100%]
10 passed

Session-key coverage (tests/test_session_waiter_cross_user.py):

  1. test_default_filter_returns_umo_unchanged — the default filter still returns
    unified_msg_origin (locks the default semantics in place);
  2. test_default_manual_umo_registration_still_triggers — reproduces the
    kimi_datasource_api manual-registration pattern and asserts it still
    triggers (guards against regressing plugins that depend on the group default);
  3. test_sender_filter_isolates_users_in_group — another member's message is no
    longer intercepted (primary goal);
  4. test_sender_filter_accepts_original_sender — the original sender's follow-up
    is still captured;
  5. test_sender_filter_key_is_collision_free — UMOs / sender ids containing
    separator characters (#, :) cannot produce colliding keys;
  6. test_sender_filter_isolates_users_with_hash_in_umo — isolation holds for
    Telegram topic-group style UMOs that contain #;
  7. test_sender_filter_does_not_leak_across_conversations — the same user's
    messages in other groups / DMs are not intercepted.

Production-wiring coverage (tests/unit/test_empty_mention_sender_scope.py,
drives the real handlers rather than a re-implementation):

  1. test_other_members_message_passes_through — BOB's message is not stopped,
    not modified, and not re-enqueued while ALICE's waiter is active;
  2. test_original_senders_followup_is_redispatched — ALICE's follow-up gets the
    bot At injected, is stopped and re-enqueued, and the waiter is cleaned up;
  3. test_empty_sender_id_skips_waiting — with no reliable sender id the
    handler finishes immediately and registers no waiter.

Verified that the wiring tests actually guard the fix: with the main.py
change reverted to master, tests 8 and 10 fail; with the fix in place all 10
pass.


  • This is NOT a breaking change.

The default session scope is unchanged; only the built-in empty-mention waiter
opts into per-sender isolation. SenderSessionFilter is an additive core
utility and is not exported through astrbot.api.util.

Checklist

Summary by Sourcery

Fix empty-mention session handling in group chats by isolating the waiter per sender while preserving existing group-level session semantics.

Bug Fixes:

  • Prevent empty-mention waiters in group chats from intercepting follow-up messages from other members and replying in the wrong context.
  • Ensure empty-mention handling skips session waiting when no reliable sender ID is available to avoid unintended group-scoped behavior.

Enhancements:

  • Introduce an optional SenderSessionFilter that scopes session keys by conversation origin and sender using a collision-safe encoding without changing the default session filter.
  • Strengthen session key behavior with explicit guarantees against key collisions and cross-conversation leakage.

Tests:

  • Add regression tests that lock in DefaultSessionFilter behavior and validate SenderSessionFilter isolation, collision safety, and cross-conversation separation.
  • Add production wiring tests for empty-mention handling to verify correct per-sender redispatch, non-interference with other members, and behavior when sender ID is missing.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:core The bug / feature is about astrbot's core, backend labels Jul 24, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@xiaoyuyu6420 xiaoyuyu6420 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

I've reviewed this PR in the context of issue #9377 alongside the two other open PRs (#9422, #9442) that address the same bug. Here's my analysis.

Strengths

  1. Length-prefixed key encoding ({len(umo)}:{umo}:{sender_id}) — this is technically the most robust approach to key collision resistance. Telegram topic-group origins contain #, and : appears in all UMO formats (platform:MessageType:id), so plain concatenation with a separator has a theoretical collision surface. This encoding is uniquely decodable.

  2. Comprehensive test suite — two separate test files covering both unit-level key logic and integration-level Main.handle_empty_mention wiring. The regression guard for astrbot_plugin_kimi_datasource_api (manual UMO registration pattern) is particularly valuable.

  3. Opt-in design preserves backward compatibilityDefaultSessionFilter is unchanged, so third-party plugins that rely on group-level keys are not affected.

Issues

1. Does not fix the non-text message swallowing bug (mentioned in #9377 as a "连带问题")

The issue report explicitly documents that pure image messages during the wait window are silently dropped and the waiter stays alive:

empty_mention_waiter handler:
    if not event.message_str or not event.message_str.strip():
        return  # ← pure image is silently dropped, waiter keeps running

This PR only adds SenderSessionFilter to the call site but does not modify the handler body. After this fix, Alice's pure image in the wait window is still silently consumed (by Alice herself now, not by Bob — an improvement, but the message is still lost).

2. Does not fix the _cleanup race condition (mentioned in #9377 as a "连带问题")

The issue report documents that SessionWaiter._cleanup() unconditionally calls USER_SESSIONS.pop(self.session_id). If a newer waiter is registered under the same key before the old one times out, the old waiter's cleanup evicts the new one. This PR does not address this.

3. Skipping the waiter on empty sender_id is a behavioral regression

if str(event.get_sender_id()):
    await empty_mention_waiter(event, session_filter=SenderSessionFilter())

When sender_id is empty/falsy, the waiter is simply not started. On platforms where get_sender_id() may temporarily return empty, the bot silently stops responding to empty mentions with no feedback to the user. A fallback placeholder (e.g., <unknown>) with a warning log would be safer — it degrades gracefully instead of silently dropping functionality.

4. Only fixes empty_mention_waiter call site, not is_wake_prefix_only

The issue report notes: "仅含唤醒前缀的消息(is_wake_prefix_only 分支)会启动同一个等待器,跨用户截获问题同样适用于该场景". The same session_waiter is used in both branches, but this PR only passes SenderSessionFilter to the empty_mention_waiter call. The wake-prefix-only path still uses DefaultSessionFilter and is still vulnerable to cross-user interception.

Summary

This PR correctly identifies the core isolation problem and proposes a clean opt-in API with excellent test coverage. However, it addresses only one of the three issues documented in #9377 (cross-user interception), leaving the non-text swallowing and cleanup race unfixed. The empty-sender-id skip also introduces a minor behavioral regression.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8597ae2751

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".



async def finish(task: asyncio.Task) -> None:
task.cancel()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Inline the single-use task cleanup

finish() is called only once and merely wraps a short, linear cancellation block, so extracting it fragments the test without meeting the repository’s requirement that helpers have at least three reuse sites or prevent extreme complexity. Inline this cleanup at its call site instead.

AGENTS.md reference: AGENTS.md:L67-L72

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38e036c065

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return main


async def start_empty_mention(main: Main, event: FakeEvent) -> asyncio.Task:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Inline the two-use waiter startup helper

start_empty_mention() is used by only two tests and wraps a short startup sequence, while also introducing the single-use nested drain() helper. Neither helper reaches the required three reuse sites or avoids extreme complexity, so inline this setup into the two tests instead.

AGENTS.md reference: AGENTS.md:L65-L73

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 群聊中空提及等待器会截获其他成员的消息(session_waiter 未绑定发送者)

2 participants